昨天我們學會了 TDD 的紅綠重構循環,體驗了從無到有開發功能的完整流程。隨著測試越寫越多,你可能開始感到困擾:「這些測試散落各處,很難找到我要的測試」、「類似的測試重複出現,但又不完全一樣」。
想像一個場景:你的數學工具庫現在有 20 個方法,每個方法有 5-8 個測試案例,總共 100 多個測試。當某個測試失敗時,你需要在一大堆 it
中找到問題所在,這時你就會深刻體會到「測試結構」的重要性。
今天我們要學習如何組織測試,讓測試代碼變得清晰、有條理,就像整理房間一樣 —— 相關的東西放在一起,每樣東西都有固定位置。
今天結束後,你將學會:
第一階段:打好基礎(Day 1-10)
├── Day 01 - 環境設置與第一個測試
├── Day 02 - 認識斷言(Assertions)
├── Day 03 - TDD 紅綠重構循環
├── Day 04 - 測試結構與組織 ★ 今天在這裡
├── ...
└── (更多精彩內容待續)
回想昨天的數學工具庫,如果我們繼續用扁平的方式寫測試:
// 混亂的測試結構
it('isPrime with 2 should return true')
it('isPrime with 3 should return true')
it('isPrime with 4 should return false')
it('countPrimesInRange with range 1-10')
it('isPrime with negative number should return false')
it('fibonacci with 0 should return 0')
問題:
isPrime
的測試散落各處// 結構化的測試組織
describe('math utilities', function() {
describe('isPrime', function() {
describe('prime detection', function() {
it('identifies small prime numbers');
it('identifies large prime numbers');
});
describe('composite detection', function() {
it('identifies small composite numbers');
it('identifies large composite numbers');
});
describe('boundary cases', function() {
it('handles negative numbers');
it('handles zero and one');
});
});
});
好處:
測試套件是一組相關測試的集合,用來驗證特定功能或類別。在 Pest 中,我們用 describe
來創建測試套件:
describe('function name', function() {
it('test case 1');
it('test case 2');
});
describe('math utilities', function() { // 頂層套件:類別
describe('isPrime', function() { // 第二層:方法
describe('positive number tests', function() { // 第三層:測試分類
it('detects small prime numbers'); // 具體測試案例
it('detects large prime numbers');
});
describe('boundary tests', function() {
it('handles zero');
it('handles negative numbers');
});
});
});
這種結構讓測試報告更易讀:
數學工具庫
├── isPrime
│ ├── 正數測試
│ │ ✓ 檢測小質數
│ │ ✓ 檢測大質數
│ └── 邊界測試
│ ✓ 處理零
│ ✓ 處理負數
讓我們把昨天的測試重新組織。建立 tests/Unit/Day04/MathUtilsTest.php
:
<?php
use App\MathUtils;
describe('MathUtils', function() {
describe('isPrime', function() {
// 所有 isPrime 相關的測試都放這裡
});
describe('countPrimesInRange', function() {
// 所有 countPrimesInRange 相關的測試都放這裡
});
});
建立 tests/Unit/Day04/MathUtilsTest.php
:
describe('isPrime', function() {
describe('prime numbers', function() {
it('identifies small primes', function() {
expect(MathUtils::isPrime(2))->toBeTrue();
expect(MathUtils::isPrime(3))->toBeTrue();
expect(MathUtils::isPrime(5))->toBeTrue();
expect(MathUtils::isPrime(7))->toBeTrue();
});
it('identifies larger primes', function() {
expect(MathUtils::isPrime(11))->toBeTrue();
expect(MathUtils::isPrime(13))->toBeTrue();
expect(MathUtils::isPrime(17))->toBeTrue();
expect(MathUtils::isPrime(19))->toBeTrue();
});
});
describe('composite numbers', function() {
it('identifies small composites', function() {
expect(MathUtils::isPrime(4))->toBeFalse();
expect(MathUtils::isPrime(6))->toBeFalse();
expect(MathUtils::isPrime(8))->toBeFalse();
expect(MathUtils::isPrime(9))->toBeFalse();
});
});
describe('boundary cases', function() {
it('handles numbers less than 2', function() {
expect(MathUtils::isPrime(0))->toBeFalse();
expect(MathUtils::isPrime(1))->toBeFalse();
expect(MathUtils::isPrime(-1))->toBeFalse();
});
});
});
最常見的分組方式是按方法分組:
describe('MathUtils', function() {
describe('isPrime', function() { /* ... */ });
describe('fibonacci', function() { /* ... */ });
describe('factorial', function() { /* ... */ });
});
在每個功能內,再按測試類型細分:
describe('isPrime', function() {
describe('positive tests', function() {
// 正常情況的測試
});
describe('boundary tests', function() {
// 邊界值的測試
});
describe('error handling', function() {
// 異常情況的測試(Day 8 會詳細學習)
});
});
describe('isPrime', function() {
// ✅ 好的命名:描述期望的行為
it('identifies prime number 2');
it('identifies composite number 4');
it('handles negative numbers');
// ❌ 不好的命名:過於技術性
it('should return true when input is 2');
});
推薦命名模式:[動詞] + [對象] + [條件]
tests/Unit/Day04/MathUtilsTest.php
<?php
use App\MathUtils;
describe('MathUtils', function() {
describe('isPrime', function() {
describe('prime numbers', function() {
it('identifies small primes', function() {
expect(MathUtils::isPrime(2))->toBeTrue();
expect(MathUtils::isPrime(3))->toBeTrue();
expect(MathUtils::isPrime(5))->toBeTrue();
expect(MathUtils::isPrime(7))->toBeTrue();
});
it('identifies larger primes', function() {
expect(MathUtils::isPrime(11))->toBeTrue();
expect(MathUtils::isPrime(13))->toBeTrue();
expect(MathUtils::isPrime(17))->toBeTrue();
expect(MathUtils::isPrime(19))->toBeTrue();
});
});
describe('composite numbers', function() {
it('identifies small composites', function() {
expect(MathUtils::isPrime(4))->toBeFalse();
expect(MathUtils::isPrime(6))->toBeFalse();
expect(MathUtils::isPrime(8))->toBeFalse();
expect(MathUtils::isPrime(9))->toBeFalse();
});
it('identifies larger composites', function() {
expect(MathUtils::isPrime(10))->toBeFalse();
expect(MathUtils::isPrime(12))->toBeFalse();
expect(MathUtils::isPrime(14))->toBeFalse();
expect(MathUtils::isPrime(15))->toBeFalse();
});
});
describe('boundary cases', function() {
it('handles numbers less than 2', function() {
expect(MathUtils::isPrime(0))->toBeFalse();
expect(MathUtils::isPrime(1))->toBeFalse();
expect(MathUtils::isPrime(-1))->toBeFalse();
expect(MathUtils::isPrime(-10))->toBeFalse();
});
});
});
describe('countPrimesInRange', function() {
describe('valid ranges', function() {
it('counts primes in small range', function() {
expect(MathUtils::countPrimesInRange(1, 10))->toBe(4);
});
it('counts primes in medium range', function() {
expect(MathUtils::countPrimesInRange(10, 20))->toBe(4);
});
});
describe('edge cases', function() {
it('handles single number range', function() {
expect(MathUtils::countPrimesInRange(2, 2))->toBe(1);
expect(MathUtils::countPrimesInRange(4, 4))->toBe(0);
});
it('handles reversed range', function() {
expect(MathUtils::countPrimesInRange(10, 1))->toBe(0);
});
});
});
});
./vendor/bin/pest tests/Unit/Day04/MathUtilsTest.php --verbose
輸出結果會顯示清晰的階層:
MathUtils
isPrime
prime numbers
✓ identifies small primes
✓ identifies larger primes
composite numbers
✓ identifies small composites
✓ identifies larger composites
boundary cases
✓ handles numbers less than 2
countPrimesInRange
valid ranges
✓ counts primes in small range
✓ counts primes in medium range
edge cases
✓ handles single number range
✓ handles reversed range
Tests: 9 passed
今天我們從散亂的測試進化到有組織的測試結構:
測試結構與組織不只是美觀,更是實用。良好的組織結構能提高開發效率、降低維護成本、改善團隊協作。記住:好的測試結構是可維護測試代碼的基礎。
明天我們將學習「測試生命週期」,了解如何在測試執行前後進行必要的設置和清理工作。 💪